#!/usr/bin/env python  
# -*- coding: iso-8859-1 -*-  
"""ping.py  
   
 ping.py uses the ICMP protocol's mandatory ECHO_REQUEST  
 datagram to elicit an ICMP ECHO_RESPONSE from a  
 host or gateway.  
 Copyright (C) 2004 - Lars Strand <lars strand at gnist org>;  
   
 This program is free software; you can redistribute it and/or  
 modify it under the terms of the GNU General Public License  
 as published by the Free Software Foundation; either version 2  
 of the License, or (at your option) any later version.  
   
 This program is distributed in the hope that it will be useful,  
 but WITHOUT ANY WARRANTY; without even the implied warranty of  
 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the  
 GNU General Public License for more details.  
   
 You should have received a copy of the GNU General Public License  
 along with this program; if not, write to the Free Software  
 Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.  
 Must be running as root, or write a suid-wrapper. Since newer *nix  
 variants, the kernel ignores the set[ug]id flags on #! scripts for  
 security reasons  
 RFC792, echo/reply message:  
  0                   1                   2                   3  
  0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1  
 +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+  
 |     Type      |     Code      |          Checksum             |  
 +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+  
 |           Identifier          |        Sequence Number        |  
 +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+  
 |     Data ...  
 +-+-+-+-+-  
  
TODO:  
 - do not create socket inside 'while' (but if not: ipv6 won't work)  
 - add support for broadcast/multicast  
 - add support for own payload string  
CHANGELOG:  
 DONE -->; bugfix from Filip Van Raemdonck mechanix debian org  
 DONE -->; add more support for modules (raise instead of sys.exit)  
 DONE -->; locale func names  
 DONE -->; package def  
 DONE -->; some code cleanup  
   
"""  
import sys  
import os  
import struct  
import array  
import time  
import select  
import binascii  
import math  
import getopt  
import string  
import socket  
# total size of data (payload)  
ICMP_DATA_STR = 56    
# initial values of header variables  
ICMP_TYPE = 8  
ICMP_TYPE_IP6 = 128  
ICMP_CODE = 0  
ICMP_CHECKSUM = 0  
ICMP_ID = 0  
ICMP_SEQ_NR = 0  
# Package definitions.  
__program__   = 'ping'  
__version__   = '0.5a'  
__date__      = '2004/15/12'  
__author__    = 'Lars Strand <lars at unik no>;'  
__licence__   = 'GPL'  
__copyright__ = 'Copyright (C) 2004 Lars Strand'  
def _construct(id, size, ipv6):  
    """Constructs a ICMP echo packet of variable size  
    """  
    # size must be big enough to contain time sent  
    if size < int(struct.calcsize("d")):  
        _error("packetsize to small, must be at least %d" % int(struct.calcsize("d")))  
      
    # construct header  
    if ipv6:  
        header = struct.pack('BbHHh', ICMP_TYPE_IP6, ICMP_CODE, ICMP_CHECKSUM,ICMP_ID, ICMP_SEQ_NR+id)  
    else:  
        header = struct.pack('bbHHh', ICMP_TYPE, ICMP_CODE, ICMP_CHECKSUM,ICMP_ID, ICMP_SEQ_NR+id)  
    # if size big enough, embed this payload  
    load = "-- IF YOU ARE READING THIS YOU ARE A NERD! --"  
      
    # space for time  
    size -= struct.calcsize("d")  
    # construct payload based on size, may be omitted :)  
    rest = ""  
    if size > len(load):  
        rest = load  
        size -= len(load)  
    # pad the rest of payload  
    rest += size * "X"  
    # pack  
    data = struct.pack("d", time.time()) + rest  
    packet = header + data          # ping packet without checksum  
    checksum = _in_cksum(packet)    # make checksum  
    # construct header with correct checksum  
    if ipv6:  
        header = struct.pack('BbHHh', ICMP_TYPE_IP6, ICMP_CODE, checksum,ICMP_ID, ICMP_SEQ_NR+id)  
    else:  
        header = struct.pack('bbHHh', ICMP_TYPE, ICMP_CODE, checksum, ICMP_ID,ICMP_SEQ_NR+id)  
    # ping packet *with* checksum  
    packet = header + data   
    # a perfectly formatted ICMP echo packet  
    return packet  
def _in_cksum(packet):  
    """THE RFC792 states: 'The 16 bit one's complement of  
    the one's complement sum of all 16 bit words in the header.'  
    Generates a checksum of a (ICMP) packet. Based on in_chksum found  
    in ping.c on FreeBSD.  
    """  
    # add byte if not dividable by 2  
    if len(packet) & 1:                
        packet = packet + '\0'  
    # split into 16-bit word and insert into a binary array  
    words = array.array('h', packet)   
    sum = 0  
    # perform ones complement arithmetic on 16-bit words  
    for word in words:  
        sum += (word & 0xffff)   
    hi = sum >> 16   
    lo = sum & 0xffff   
    sum = hi + lo  
    sum = sum + (sum >> 16)  
      
    return (~sum) & 0xffff # return ones complement  
def pingNode(alive=0, timeout=1.0, ipv6=0, number=sys.maxint, node=None,flood=0, size=ICMP_DATA_STR):  
    """Pings a node based on input given to the function.  
    """  
    # if no node, exit  
    if not node:  
        _error("")  
    # if not a valid host, exit  
    if ipv6:  
        if socket.has_ipv6:  
            try:  
                info, port = socket.getaddrinfo(node, None)  
                host = info[4][0]  
                # do not print ipv6 twice if ipv6 address given as node  
                if host == node:   
                    noPrintIPv6adr = 1  
            except:  
                _error("cannot resolve %s: Unknow host" % node)  
        else:  
            _error("No support for IPv6 on this plattform")  
    else:    # IPv4  
        try:  
            host = socket.gethostbyname(node)  
        except:  
            _error("cannot resolve %s: Unknow host" % node)  
    # trying to ping a network?  
    if not ipv6:  
        if int(string.split(host, ".")[-1]) == 0:  
            _error("no support for network ping")  
    # do some sanity check  
    if number == 0:  
        _error("invalid count of packets to transmit: '%s'" % str(a))  
    if alive:  
        number = 1  
    # Send the ping(s)  
    start = 1; mint = 999; maxt = 0.0; avg = 0.0  
    lost = 0; tsum = 0.0; tsumsq = 0.0  
    # tell the user what we do  
    if not alive:  
        if ipv6:  
            # do not print the ipv6 twice if ip adress given as node  
            # (it can be to long in term window)  
            if noPrintIPv6adr == 1:  
                # add 40 (header) + 8 (icmp header) + payload  
                print "PING %s : %d data bytes (40+8+%d)" % (str(node), \  
                                                             40+8+size, size)  
            else:  
                # add 40 (header) + 8 (icmp header) + payload  
                print "PING %s (%s): %d data bytes (40+8+%d)" % (str(node), \  
                                                                 str(host), 40+8+size, size)  
        else:  
            # add 20 (header) + 8 (icmp header) + payload  
            print "PING %s (%s): %d data bytes (20+8+%d)" % (str(node), str(host), \  
                                                             20+8+size, size)  
          
    # trap ctrl-d and ctrl-c  
    try:  
          
        # send the number of ping packets as given  
        while start <= number:  
            lost += 1 # in case user hit ctrl-c  
              
            # create the IPv6/IPv4 socket  
            if ipv6:  
                # can not create a raw socket if not root or setuid to root  
                try:  
                    pingSocket = socket.socket(socket.AF_INET6, socket.SOCK_RAW, \  
                                               socket.getprotobyname("ipv6-icmp"))  
                except socket.error, e:  
                    print "socket error: %s" % e  
                    _error("You must be root (uses raw sockets)" % os.path.basename(sys.argv[0]))  
                      
            # IPv4  
            else:  
                # can not create a raw socket if not root or setuid to root  
                try:  
                    pingSocket = socket.socket(socket.AF_INET, socket.SOCK_RAW, \  
                                               socket.getprotobyname("icmp"))  
                except socket.error, e:  
                    print "socket error: %s" % e  
                    _error("You must be root (%s uses raw sockets)" % os.path.basename(sys.argv[0]))  
                  
            packet = _construct(start, size, ipv6) # make a ping packet  
            # send the ping  
            try:  
                pingSocket.sendto(packet,(node,1))  
            except socket.error, e:  
                _error("socket error: %s" % e)  
            # reset values  
            pong = ""; iwtd = []  
            # wait until there is data in the socket  
            while 1:  
                # input, output, exceptional conditions  
                iwtd, owtd, ewtd = select.select([pingSocket], [], [], timeout)  
                break # no data and timout occurred   
            # data on socket - this means we have an answer  
            if iwtd:  # ok, data on socket  
                endtime = time.time()  # time packet received  
                # read data (we only need the header)  
                pong, address = pingSocket.recvfrom(size+48)  
                lost -= 1 # in case user hit ctrl-c  
                # examine packet  
                # fetch TTL from IP header  
                if ipv6:  
                    # since IPv6 header and any extension header are never passed  
                    # to a raw socket, we can *not* get hoplimit field..  
                    # I hoped that a socket option would help, but it's not  
                    # supported:  
                    #   pingSocket.setsockopt(IPPROTO_IPV6, IPV6_RECVHOPLIMIT, 1)  
                    # so we can't fetch hoplimit..  
                    # fetch hoplimit  
                    #rawPongHop = struct.unpack("c", pong[7])[0]  
                    # fetch pong header  
                    pongHeader = pong[0:8]  
                    pongType, pongCode, pongChksum, pongID, pongSeqnr = \  
                              struct.unpack("bbHHh", pongHeader)  
                    # fetch starttime from pong  
                    starttime = struct.unpack("d", pong[8:16])[0]  
                # IPv4  
                else:  
                    # time to live  
                    rawPongHop = struct.unpack("s", pong[8])[0]  
                    # convert TTL from 8 bit to 16 bit integer  
                    pongHop = int(binascii.hexlify(str(rawPongHop)), 16)  
                    # fetch pong header  
                    pongHeader = pong[20:28]  
                    pongType, pongCode, pongChksum, pongID, pongSeqnr = \  
                              struct.unpack("bbHHh", pongHeader)  
                    # fetch starttime from pong  
                    starttime = struct.unpack("d", pong[28:36])[0]  
                # valid ping packet received?  
                if not pongSeqnr == start:  
                    pong = None  
            # NO data on socket - timeout waiting for answer  
            if not pong:  
                if alive:  
                    print "no reply from %s (%s)" % (str(node), str(host))  
                else:  
                    print "ping timeout: %s (icmp_seq=%d) " % (host, start)  
                # do not wait if just sending one packet  
                if number != 1 and start < number:  
                    time.sleep(flood ^ 1)  
                start += 1  
                continue  # lost a packet - try again  
            triptime  = endtime - starttime # compute RRT  
            tsum     += triptime            # triptime for all packets (stddev)  
            tsumsq   += triptime * triptime # triptime^2  for all packets (stddev)  
            # compute statistic  
            maxt = max ((triptime, maxt))  
            mint = min ((triptime, mint))  
            if alive:  
                print str(node) + " (" + str(host) +") is alive"  
            else:  
                if ipv6:  
                    # size + 8 = payload + header  
                    print "%d bytes from %s: icmp_seq=%d time=%.5f ms" % \  
                          (size+8, host, pongSeqnr, triptime*1000)  
                else:  
                    print "%d bytes from %s: icmp_seq=%d ttl=%s time=%.5f ms" % \  
                          (size+8, host, pongSeqnr, pongHop, triptime*1000)  
            # do not wait if just sending one packet  
            if number != 1 and start < number:  
                # if flood = 1; do not sleep - just ping                  
                time.sleep(flood ^ 1) # wait before send new packet  
            # the last thing to do is update the counter - else the value  
            # (can) get wrong when computing summary at the end (if user  
            # hit ctrl-c when pinging)  
            start += 1  
            # end ping send/recv while  
    # if user ctrl-d or ctrl-c  
    except (EOFError, KeyboardInterrupt):  
        # if user disrupts ping, it is most likly done before  
        # the counter get updates - if do not update it here, the  
        # summary get all wrong.  
        start += 1  
        pass  
    # compute and print som stats  
    # stddev computation based on ping.c from FreeBSD  
    if start != 0 or lost > 0:  # do not print stats if 0 packet sent  
        start -= 1              # since while is '<='  
        avg = tsum / start      # avg round trip  
        vari = tsumsq / start - avg * avg   
        # %-packet lost  
        if start == lost:  
            plost = 100  
        else:  
            plost = (lost/start)*100  
        if not alive:  
            print "\n--- %s ping statistics ---" % node  
            print "%d packets transmitted, %d packets received, %d%% packet loss" % \  
                  (start, start-lost, plost)  
            # don't display summary if 100% packet-loss  
            if plost != 100:  
                print "round-trip min/avg/max/stddev = %.3f/%.3f/%.3f/%.3f ms" % \  
                      (mint*1000, (tsum/start)*1000, maxt*1000, math.sqrt(vari)*1000)  
    pingSocket.close()  
      
def _error(err):  
    """Exit if running standalone, else raise an exception  
    """  
    if __name__ == '__main__':  
        print "%s: %s" % (os.path.basename(sys.argv[0]), str(err))  
        print "Try `%s --help' for more information." % os.path.basename(sys.argv[0])  
        sys.exit(1)  
    else:  
        raise Exception, str(err)  
      
def _usage():  
    """Print usage if run as a standalone program  
    """  
    print """usage: %s [OPTIONS] HOST  
Send ICMP ECHO_REQUEST packets to network hosts.  
Mandatory arguments to long options are mandatory for short options too.  
  -c, --count=N    Stop after sending (and receiving) 'N' ECHO_RESPONSE  
                   packets.  
  -s, --size=S     Specify the number of data bytes to be sent. The default  
                   is 56, which translates into 64 ICMP data bytes when  
                   combined with the 8 bytes of ICMP header data.  
  -f, --flood      Flood ping. Outputs packets as fast as they come back. Use  
                   with caution!  
  -6, --ipv6       Ping using IPv6.  
  -t, --timeout=s  Specify a timeout, in seconds, before a ping packet is  
                   considered 'lost'.  
  -h, --help       Display this help and exit  
Report bugs to lars [at] gnist org""" % os.path.basename(sys.argv[0])  
  
if __name__ == '__main__':  
    """Main loop  
    """  
    # version control  
    version = string.split(string.split(sys.version)[0][:3], ".")  
    if map(int, version) < [2, 3]:  
        _error("You need Python ver 2.3 or higher to run!")  
    try:  
        # opts = arguments recognized,  
        # args = arguments NOT recognized (leftovers)  
        opts, args = getopt.getopt(sys.argv[1:-1], "hat:6c:fs:", \  
                                   ["help", "alive", "timeout=", "ipv6", \  
                                    "count=", "flood", "packetsize="])  
    except getopt.GetoptError:  
        # print help information and exit:  
        _error("illegal option(s) -- " + str(sys.argv[1:]))  
    # test whether any host given  
    if len(sys.argv) >= 2:  
        node = sys.argv[-1:][0]   # host to be pinged  
        if node[0] == '-' or node == '-h' or node == '--help' :    
            _usage()  
    else:  
        _error("No arguments given")  
    if args:  
        _error("illegal option -- %s" % str(args))  
          
    # default variables  
    alive = 0; timeout = 1.0; ipv6 = 0; count = sys.maxint;  
    flood = 0; size = ICMP_DATA_STR  
    # run through arguments and set variables  
    for o, a in opts:  
        if o == "-h" or o == "--help":    # display help and exit  
            _usage()  
            sys.exit(0)  
        if o == "-t" or o == "--timeout": # timeout before "lost"  
            try:  
                timeout = float(a)  
            except:  
                _error("invalid timout: '%s'" % str(a))  
        if o == "-6" or o == "--ipv6":    # ping ipv6  
            ipv6 = 1  
        if o == "-c" or o == "--count":   # how many pings?  
            try:  
                count = int(a)  
            except:  
                _error("invalid count of packets to transmit: '%s'" % str(a))  
        if o == "-f" or o == "--flood":   # no delay between ping send  
            flood = 1  
        if o == "-s" or o == "--packetsize":  # set the ping payload size  
            try:  
                size = int(a)  
            except:  
                _error("invalid packet size: '%s'" % str(a))  
        # just send one packet and say "it's alive"  
        if o == "-a" or o == "--alive":     
            alive = 1  
    # here we send  
    pingNode(alive=alive, timeout=timeout, ipv6=ipv6, number=count,node=node, flood=flood, size=size)  
    # if we made it this far, do a clean exit  
    sys.exit(0)  
### end 
